//
//  ESPullToRefresh.swift
//
//  Created by egg swift on 16/4/7.
//  Copyright (c) 2013-2016 ESPullToRefresh (https://github.com/eggswift/pull-to-refresh)
//
//  Permission is hereby granted, free of charge, to any person obtaining a copy
//  of this software and associated documentation files (the "Software"), to deal
//  in the Software without restriction, including without limitation the rights
//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
//  copies of the Software, and to permit persons to whom the Software is
//  furnished to do so, subject to the following conditions:
//
//  The above copyright notice and this permission notice shall be included in
//  all copies or substantial portions of the Software.
//
//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
//  THE SOFTWARE.
//

import Foundation
import UIKit

private var kESRefreshHeaderKey: Void?
private var kESRefreshFooterKey: Void?

public extension UIScrollView {
  
  /// Pull-to-refresh associated property
  var header: ESRefreshHeaderView? {
    get { return (objc_getAssociatedObject(self, &kESRefreshHeaderKey) as? ESRefreshHeaderView) }
    set(newValue) { objc_setAssociatedObject(self, &kESRefreshHeaderKey, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN) }
  }
  
  /// Infinitiy scroll associated property
  var footer: ESRefreshFooterView? {
    get { return (objc_getAssociatedObject(self, &kESRefreshFooterKey) as? ESRefreshFooterView) }
    set(newValue) { objc_setAssociatedObject(self, &kESRefreshFooterKey, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN) }
  }
}

public extension ES where Base: UIScrollView {
  /// Add pull-to-refresh
  @discardableResult
  func addPullToRefresh(handler: @escaping ESRefreshHandler) -> ESRefreshHeaderView {
    removeRefreshHeader()
    let header = ESRefreshHeaderView(frame: CGRect.zero, handler: handler)
    let headerH = header.animator.executeIncremental
    header.frame = CGRect.init(x: 0.0, y: -headerH /* - contentInset.top */, width: self.base.bounds.size.width, height: headerH)
    self.base.addSubview(header)
    self.base.header = header
    return header
  }
  
  @discardableResult
  func addPullToRefresh(animator: ESRefreshProtocol & ESRefreshAnimatorProtocol, handler: @escaping ESRefreshHandler) -> ESRefreshHeaderView {
    removeRefreshHeader()
    let header = ESRefreshHeaderView(frame: CGRect.zero, handler: handler, animator: animator)
    let headerH = animator.executeIncremental
    header.frame = CGRect.init(x: 0.0, y: -headerH /* - contentInset.top */, width: self.base.bounds.size.width, height: headerH)
    self.base.addSubview(header)
    self.base.header = header
    return header
  }
  
  /// Add infinite-scrolling
  @discardableResult
  func addInfiniteScrolling(handler: @escaping ESRefreshHandler) -> ESRefreshFooterView {
    removeRefreshFooter()
    let footer = ESRefreshFooterView(frame: CGRect.zero, handler: handler)
    let footerH = footer.animator.executeIncremental
    footer.frame = CGRect.init(x: 0.0, y: self.base.contentSize.height + self.base.contentInset.bottom, width: self.base.bounds.size.width, height: footerH)
    self.base.addSubview(footer)
    self.base.footer = footer
    return footer
  }
  
  @discardableResult
  func addInfiniteScrolling(animator: ESRefreshProtocol & ESRefreshAnimatorProtocol, handler: @escaping ESRefreshHandler) -> ESRefreshFooterView {
    removeRefreshFooter()
    let footer = ESRefreshFooterView(frame: CGRect.zero, handler: handler, animator: animator)
    let footerH = footer.animator.executeIncremental
    footer.frame = CGRect.init(x: 0.0, y: self.base.contentSize.height + self.base.contentInset.bottom, width: self.base.bounds.size.width, height: footerH)
    self.base.footer = footer
    self.base.addSubview(footer)
    return footer
  }
  
  /// Remove
  func removeRefreshHeader() {
    self.base.header?.stopRefreshing()
    self.base.header?.removeFromSuperview()
    self.base.header = nil
  }
  
  func removeRefreshFooter() {
    self.base.footer?.stopRefreshing()
    self.base.footer?.removeFromSuperview()
    self.base.footer = nil
  }
  
  /// Manual refresh
  func startPullToRefresh() {
    DispatchQueue.main.async { [weak base] in
      base?.header?.startRefreshing(isAuto: false)
    }
  }
  
  /// Auto refresh if expired.
  func autoPullToRefresh() {
    if self.base.expired == true {
      DispatchQueue.main.async { [weak base] in
        base?.header?.startRefreshing(isAuto: true)
      }
    }
  }
  
  /// Stop pull to refresh
  func stopPullToRefresh(ignoreDate: Bool = false, ignoreFooter: Bool = false) {
    self.base.header?.stopRefreshing()
    if ignoreDate == false {
      if let key = self.base.header?.refreshIdentifier {
        ESRefreshDataManager.sharedManager.setDate(Date(), forKey: key)
      }
      self.base.footer?.resetNoMoreData()
    }
    self.base.footer?.isHidden = ignoreFooter
  }
  
  /// Footer notice method
  func  noticeNoMoreData() {
    self.base.footer?.stopRefreshing()
    self.base.footer?.noMoreData = true
  }
  
  func resetNoMoreData() {
    self.base.footer?.noMoreData = false
  }
  
  func stopLoadingMore() {
    self.base.footer?.stopRefreshing()
  }
  
}

public extension UIScrollView /* Date Manager */ {
  
  /// Identifier for cache expired timeinterval and last refresh date.
  var refreshIdentifier: String? {
    get { return self.header?.refreshIdentifier }
    set { self.header?.refreshIdentifier = newValue }
  }
  
  /// If you setted refreshIdentifier and expiredTimeInterval, return nearest refresh expired or not. Default is false.
  var expired: Bool {
    get {
      if let key = self.header?.refreshIdentifier {
        return ESRefreshDataManager.sharedManager.isExpired(forKey: key)
      }
      return false
    }
  }
  
  var expiredTimeInterval: TimeInterval? {
    get {
      if let key = self.header?.refreshIdentifier {
        let interval = ESRefreshDataManager.sharedManager.expiredTimeInterval(forKey: key)
        return interval
      }
      return nil
    }
    set {
      if let key = self.header?.refreshIdentifier {
        ESRefreshDataManager.sharedManager.setExpiredTimeInterval(newValue, forKey: key)
      }
    }
  }
  
  /// Auto cached last refresh date when you setted refreshIdentifier.
  var lastRefreshDate: Date? {
    get {
      if let key = self.header?.refreshIdentifier {
        return ESRefreshDataManager.sharedManager.date(forKey: key)
      }
      return nil
    }
  }
  
}

open class ESRefreshHeaderView: ESRefreshComponent {
  
  fileprivate var previousOffset: CGFloat = 0.0
  fileprivate var scrollViewInsets: UIEdgeInsets = UIEdgeInsets.zero
  fileprivate var scrollViewBounces: Bool = true
  
  open var lastRefreshTimestamp: TimeInterval?
  open var refreshIdentifier: String?
  
  public convenience init(frame: CGRect, handler: @escaping ESRefreshHandler) {
    self.init(frame: frame)
    self.handler = handler
    self.animator = ESRefreshHeaderAnimator.init()
  }
  
  open override func didMoveToSuperview() {
    super.didMoveToSuperview()
    DispatchQueue.main.async {
      [weak self] in
      self?.scrollViewBounces = self?.scrollView?.bounces ?? true
      self?.scrollViewInsets = self?.scrollView?.contentInset ?? UIEdgeInsets.zero
    }
  }
  
  open override func offsetChangeAction(object: AnyObject?, change: [NSKeyValueChangeKey : Any]?) {
    guard let scrollView = scrollView else {
      return
    }
    
    super.offsetChangeAction(object: object, change: change)
    
    guard self.isRefreshing == false && self.isAutoRefreshing == false else {
      let top = scrollViewInsets.top
      let offsetY = scrollView.contentOffset.y
      let height = self.frame.size.height
      var scrollingTop = (-offsetY > top) ? -offsetY : top
      scrollingTop = (scrollingTop > height + top) ? (height + top) : scrollingTop
      
      scrollView.contentInset.top = scrollingTop
      
      return
    }
    
    // Check needs re-set animator's progress or not.
    var isRecordingProgress = false
    defer {
      if isRecordingProgress == true {
        let percent = -(previousOffset + scrollViewInsets.top) / self.animator.trigger
        self.animator.refresh(view: self, progressDidChange: percent)
      }
    }
    
    let offsets = previousOffset + scrollViewInsets.top
    if offsets < -self.animator.trigger {
      // Reached critical
      if isRefreshing == false && isAutoRefreshing == false {
        if scrollView.isDragging == false {
          // Start to refresh...
          self.startRefreshing(isAuto: false)
          self.animator.refresh(view: self, stateDidChange: .refreshing)
        } else {
          // Release to refresh! Please drop down hard...
          self.animator.refresh(view: self, stateDidChange: .releaseToRefresh)
          isRecordingProgress = true
        }
      }
    } else if offsets < 0 {
      // Pull to refresh!
      if isRefreshing == false && isAutoRefreshing == false {
        self.animator.refresh(view: self, stateDidChange: .pullToRefresh)
        isRecordingProgress = true
      }
    } else {
      // Normal state
    }
    
    previousOffset = scrollView.contentOffset.y
    
  }
  
  open override func start() {
    guard let scrollView = scrollView else {
      return
    }
    
    // ignore observer
    self.ignoreObserver(true)
    
    // stop scroll view bounces for animation
    scrollView.bounces = false
    
    // call super start
    super.start()
    
    self.animator.refreshAnimationBegin(view: self)
    
    // 缓存scrollview当前的contentInset, 并根据animator的executeIncremental属性计算刷新时所需要的contentInset，它将在接下来的动画中应用。
    // Tips: 这里将self.scrollViewInsets.top更新，也可以将scrollViewInsets整个更新，因为left、right、bottom属性都没有用到，如果接下来的迭代需要使用这三个属性的话，这里可能需要额外的处理。
    var insets = scrollView.contentInset
    self.scrollViewInsets.top = insets.top
    insets.top += animator.executeIncremental
    
    // We need to restore previous offset because we will animate scroll view insets and regular scroll view animating is not applied then.
    scrollView.contentInset = insets
    scrollView.contentOffset.y = previousOffset
    previousOffset -= animator.executeIncremental
    UIView.animate(withDuration: 0.2, delay: 0.0, options: .curveLinear, animations: {
      scrollView.contentOffset.y = -insets.top
    }, completion: { (finished) in
      self.handler?()
      // un-ignore observer
      self.ignoreObserver(false)
      scrollView.bounces = self.scrollViewBounces
    })
    
  }
  
  open override func stop() {
    guard let scrollView = scrollView else {
      return
    }
    
    // ignore observer
    self.ignoreObserver(true)
    
    self.animator.refreshAnimationEnd(view: self)
    
    // Back state
    scrollView.contentInset.top = self.scrollViewInsets.top
    scrollView.contentOffset.y =  self.scrollViewInsets.top + self.previousOffset
    UIView.animate(withDuration: 0.2, delay: 0, options: .curveLinear, animations: {
      scrollView.contentOffset.y = -self.scrollViewInsets.top
    }, completion: { (finished) in
      self.animator.refresh(view: self, stateDidChange: .pullToRefresh)
      super.stop()
      scrollView.contentInset.top = self.scrollViewInsets.top
      self.previousOffset = scrollView.contentOffset.y
      // un-ignore observer
      self.ignoreObserver(false)
    })
  }
  
}

open class ESRefreshFooterView: ESRefreshComponent {
  fileprivate var scrollViewInsets: UIEdgeInsets = UIEdgeInsets.zero
  open var noMoreData = false {
    didSet {
      if noMoreData != oldValue {
        self.animator.refresh(view: self, stateDidChange: noMoreData ? .noMoreData : .pullToRefresh)
      }
    }
  }
  
  open override var isHidden: Bool {
    didSet {
      if isHidden == true {
        scrollView?.contentInset.bottom = scrollViewInsets.bottom
        var rect = self.frame
        rect.origin.y = scrollView?.contentSize.height ?? 0.0
        self.frame = rect
      } else {
        scrollView?.contentInset.bottom = scrollViewInsets.bottom + animator.executeIncremental
        var rect = self.frame
        rect.origin.y = scrollView?.contentSize.height ?? 0.0
        self.frame = rect
      }
    }
  }
  
  public convenience init(frame: CGRect, handler: @escaping ESRefreshHandler) {
    self.init(frame: frame)
    self.handler = handler
    self.animator = ESRefreshFooterAnimator.init()
  }
  
  /**
   In didMoveToSuperview, it will cache superview(UIScrollView)'s contentInset and update self's frame.
   It called ESRefreshComponent's didMoveToSuperview.
   */
  open override func didMoveToSuperview() {
    super.didMoveToSuperview()
    DispatchQueue.main.async {
      [weak self] in
      self?.scrollViewInsets = self?.scrollView?.contentInset ?? UIEdgeInsets.zero
      self?.scrollView?.contentInset.bottom = (self?.scrollViewInsets.bottom ?? 0) + (self?.bounds.size.height ?? 0)
      var rect = self?.frame ?? CGRect.zero
      rect.origin.y = self?.scrollView?.contentSize.height ?? 0.0
      self?.frame = rect
    }
  }
  
  open override func sizeChangeAction(object: AnyObject?, change: [NSKeyValueChangeKey : Any]?) {
    guard let scrollView = scrollView else { return }
    super.sizeChangeAction(object: object, change: change)
    let targetY = scrollView.contentSize.height + scrollViewInsets.bottom
    if self.frame.origin.y != targetY {
      var rect = self.frame
      rect.origin.y = targetY
      self.frame = rect
    }
  }
  
  open override func offsetChangeAction(object: AnyObject?, change: [NSKeyValueChangeKey : Any]?) {
    guard let scrollView = scrollView else {
      return
    }
    
    super.offsetChangeAction(object: object, change: change)
    
    guard isRefreshing == false && isAutoRefreshing == false && noMoreData == false && isHidden == false else {
      // 正在loading more或者内容为空时不相应变化
      return
    }
    
    if scrollView.contentSize.height <= 0.0 || scrollView.contentOffset.y + scrollView.contentInset.top <= 0.0 {
      self.alpha = 0.0
      return
    } else {
      self.alpha = 1.0
    }
    
    if scrollView.contentSize.height + scrollView.contentInset.top > scrollView.bounds.size.height {
      // 内容超过一个屏幕 计算公式，判断是不是在拖在到了底部
      if scrollView.contentSize.height - scrollView.contentOffset.y + scrollView.contentInset.bottom  <= scrollView.bounds.size.height {
        self.animator.refresh(view: self, stateDidChange: .refreshing)
        self.startRefreshing()
      }
    } else {
      //内容没有超过一个屏幕，这时拖拽高度大于1/2footer的高度就表示请求上拉
      if scrollView.contentOffset.y + scrollView.contentInset.top >= animator.trigger / 2.0 {
        self.animator.refresh(view: self, stateDidChange: .refreshing)
        self.startRefreshing()
      }
    }
  }
  
  open override func start() {
    guard let scrollView = scrollView else {
      return
    }
    super.start()
    
    self.animator.refreshAnimationBegin(view: self)
    
    let x = scrollView.contentOffset.x
    let y = max(0.0, scrollView.contentSize.height - scrollView.bounds.size.height + scrollView.contentInset.bottom)
    
    // Call handler
    UIView.animate(withDuration: 0.3, delay: 0.0, options: .curveLinear, animations: {
      scrollView.contentOffset = CGPoint.init(x: x, y: y)
    }, completion: { (animated) in
      self.handler?()
    })
  }
  
  open override func stop() {
    guard let scrollView = scrollView else {
      return
    }
    
    self.animator.refreshAnimationEnd(view: self)
    
    // Back state
    UIView.animate(withDuration: 0.3, delay: 0, options: .curveLinear, animations: {
    }, completion: { (finished) in
      if self.noMoreData == false {
        self.animator.refresh(view: self, stateDidChange: .pullToRefresh)
      }
      super.stop()
    })
    
    // Stop deceleration of UIScrollView. When the button tap event is caught, you read what the [scrollView contentOffset].x is, and set the offset to this value with animation OFF.
    // http://stackoverflow.com/questions/2037892/stop-deceleration-of-uiscrollview
    if scrollView.isDecelerating {
      var contentOffset = scrollView.contentOffset
      contentOffset.y = min(contentOffset.y, scrollView.contentSize.height - scrollView.frame.size.height)
      if contentOffset.y < 0.0 {
        contentOffset.y = 0.0
        UIView.animate(withDuration: 0.1, animations: { 
          scrollView.setContentOffset(contentOffset, animated: false)
        })
      } else {
        scrollView.setContentOffset(contentOffset, animated: false)
      }
    }
    
  }
  
  /// Change to no-more-data status.
  open func noticeNoMoreData() {
    self.noMoreData = true
  }
  
  /// Reset no-more-data status.
  open func resetNoMoreData() {
    self.noMoreData = false
  }
  
}
